//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace Microsoft.Research.ScientificWorkflow.TridentComposer
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Text;
    using Microsoft.Research.ScientificWorkflow.TridentUtilities;
    using Er = Microsoft.Research.DataLayer;

    /// <summary>
    /// This class is resposible for importing the Data reference provider.
    /// </summary>
    public class ExternalRegistry
    {
        /// <summary>
        /// Instance of registry connection.
        /// </summary>
        private Er.Connection registryConnection;

        /// <summary>
        /// Instance of ActivityComposer.
        /// </summary>
        private ActivityComposer activityComposer;

        /// <summary>
        /// Initializes a new instance of the <see cref="ExternalRegistry"/> class.
        /// </summary>
        /// <param name="registryConnection">The registry connection.</param>
        /// <param name="installerInstance">if set to <c>true</c> [installer instance].</param>
        public ExternalRegistry(Er.Connection registryConnection, bool installerInstance)
        {
            this.registryConnection = registryConnection;
            this.activityComposer = new ActivityComposer(registryConnection, installerInstance);
        }

        /// <summary>
        /// Imports the data reference provider.
        /// </summary>
        /// <param name="assemblyPath">The assembly path.</param>
        /// <returns></returns>
        public bool? ImportDataReferenceProvider(string assemblyPath)
        {
            if (string.IsNullOrEmpty(assemblyPath))
            {
                throw new ArgumentNullException("assemblyPath");
            }

            if (File.Exists(assemblyPath))
            {
                try
                {
                    Assembly providerAssembly = AssemblyLoad.LoadFrom(assemblyPath);

                    if (ExternalRegistry.IsDataProvider(providerAssembly))
                    {
                        Er.AssemblyPackage asmPkg = this.GetDataProviderPackage(providerAssembly);

                        if (asmPkg == null)
                        {
                            return false;
                        }

                        // Get a list of the types in the assembly which are already present in registry.
                        // Storing this list saves another ping to registry to retrieve values.
                        List<Er.DataReferenceProvider> registryDataProvidersList = this.GetExistingDataProvidersList(providerAssembly);

                        // Build a list of types in the registry to show to the user.
                        StringBuilder dataProvidersIssueList = new StringBuilder();
                        foreach (Er.DataReferenceProvider dataProvider in registryDataProvidersList)
                        {
                            dataProvidersIssueList.Append(dataProvider.Name);
                            dataProvidersIssueList.Append(", ");
                        }

                        // Get a confirmation from the user if he wants to replace the types in the registry.
                        // If yes then continue else do not add any of the types.
                        if (!string.IsNullOrEmpty(dataProvidersIssueList.ToString()))
                        {
                            string userMessage = TridentResourceManager.GetString(
                                "DataReferenceProviderOverwrite",
                                dataProvidersIssueList.ToString().Substring(0, dataProvidersIssueList.Length - 2));

                            if (TridentMessageBox.ShowTridentConfirmationYesNoMessageBox(userMessage) == System.Windows.MessageBoxResult.No)
                            {
                                return null;
                            }
                        }

                        // Iterate through the assembly for types.
                        foreach (System.Type customType in providerAssembly.GetTypes())
                        {
                            // Search only the BaseTypeInitializer types.
                            if (typeof(Er.IDataProvider).IsAssignableFrom(customType))
                            {
                                Er.IDataProvider providerInstance = providerAssembly.CreateInstance(customType.FullName) as Er.IDataProvider;

                                Er.DataReferenceProvider regType = registryDataProvidersList
                                    .FirstOrDefault(provider => provider.Name.Equals(providerInstance.Name, StringComparison.OrdinalIgnoreCase));


                                // If type is in registry then update else create new,
                                if (regType != null)
                                {
                                    ExternalRegistry.UpdateDataProvider(regType, providerInstance);
                                }
                                else
                                {
                                    regType = this.CreateDataProvider(providerInstance);
                                }

                                // Clear existing packages, and add the current package.
                                regType.AssemblyPackages.Clear();

                                regType.Save();
                                regType.AssemblyPackages.Add(asmPkg);
                            }
                        }
                    }
                    else
                    {
                        TridentErrorHandler.ThrowTridentCustomException(
                            TridentErrorConstants.ErrorNumber1400000004,
                            TridentResourceManager.GetString("NotADataReferenceProvider"));
                    }
                }
                catch (BadImageFormatException ex)
                {
                    TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber1400000002, ex.Message);
                }
                catch (FileLoadException ex)
                {
                    TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber1400000003, ex.Message);
                }
                catch (TargetInvocationException)
                {
                    TridentErrorHandler.ThrowTridentCustomException(
                        TridentErrorConstants.ErrorNumber4000026,
                        TridentResourceManager.GetString("ImportDataReferenceProviderFailed"));
                }
            }

            return true;
        }

        /// <summary>
        /// Gets the data provider package.
        /// </summary>
        /// <param name="providerAssembly">The provider assembly.</param>
        /// <returns></returns>
        private Er.AssemblyPackage GetDataProviderPackage(Assembly providerAssembly)
        {
            #region Validation of Arguments

            if (providerAssembly == null)
            {
                throw new ArgumentNullException("providerAssembly");
            }

            #endregion

            Collection<FileReference> references = new Collection<FileReference>();
            Helper.BuildReferences(references, providerAssembly.Location, Path.GetDirectoryName(providerAssembly.Location));

            FileReference fileRef = references.FirstOrDefault
                (reference => reference.DisplayName.Equals(Helper.GetFileName(providerAssembly.Location), StringComparison.OrdinalIgnoreCase));

            Er.AssemblyImage asmImg = null;
            Er.AssemblyPackage asmPkg = null;

            // Get the Assembly package from registry.
            string latestNamespace = Path.GetFileNameWithoutExtension(fileRef.FilePath);
            asmPkg = this.GetAssemblyPackage(latestNamespace);

            #region Get Assembly Image
            
            // Get the Assembly image related to the Assembly.
            FileInfo file = new FileInfo(providerAssembly.Location);
            if (null != file)
            {
                asmImg = this.activityComposer.CreateAssemblyImage(providerAssembly.FullName, file);
            }

            // Add the assembly image to the Assembly package.
            asmPkg.AssemblyImages.Add(asmImg);

            #region Get Refererenced Assembly Images
            foreach (string path in fileRef.References)
            {
                if (Helper.IsAssemblyFile(path))
                {
                    if (Helper.IsManagedAssembly(path))
                    {
                        // Get the Assembly imgae related to the Assembly.
                        Assembly refAssembly = AssemblyLoad.LoadFrom(path);
                        asmImg = this.activityComposer.GetAssemblyImage(refAssembly);
                    }
                    else
                    {
                        asmImg = this.activityComposer.GetAssemblyImage(path);
                    }
                }

                if (asmImg != null && !ActivityComposer.IsAssemblyImageAvailable(asmPkg.AssemblyImages, asmImg))
                {
                    // Add the reference of the Assembly Image to the Assembly package.
                    asmPkg.AssemblyImages.Add(asmImg);
                }
            }
            #endregion
            #endregion

            asmPkg.Refresh();

            return asmPkg;
        }

        /// <summary>
        /// This function is used to retrieve the Assembly package related 
        /// to the the activity.
        /// If the Assembly package is not present then it is going to create 
        /// the Assembly package and then return newly created instance.
        /// </summary>
        /// <param name="packageName">Name of the assembly package.</param>
        /// <returns>Assembly package instance.</returns>
        private Er.AssemblyPackage GetAssemblyPackage(string packageName)
        {
            Er.AssemblyPackage asmPkg = null;

            try
            {
                if (Er.AssemblyPackage.Exists(packageName, this.registryConnection))
                {
                    // Load the assembly package.
                    asmPkg = Er.AssemblyPackage.Load(packageName, this.registryConnection);
                    asmPkg.AssemblyImages.Clear();
                }
                else
                {
                    // Create the assembly package.
                    asmPkg = Er.AssemblyPackage.Create(packageName, this.registryConnection);
                }

                // Save the assembly package.
                asmPkg.Save();
            }
            catch (Exception exp)
            {
                TridentErrorHandler.HandleAndThrowBusinessLogicException(
                              exp, TridentErrorConstants.ErrorNumber1400000015, exp.Message);
            }

            return asmPkg;
        }

        /// <summary>
        /// Gets the existing data providers list.
        /// </summary>
        /// <param name="typesAssembly">The types assembly.</param>
        /// <returns></returns>
        private List<Er.DataReferenceProvider> GetExistingDataProvidersList(Assembly typesAssembly)
        {
            List<Er.DataReferenceProvider> dataProvidersList = new List<Er.DataReferenceProvider>();

            // Iterate through all the assemblies.
            foreach (System.Type customType in typesAssembly.GetTypes())
            {
                // Search only the activity types.
                if (typeof(Er.IDataProvider).IsAssignableFrom(customType))
                {
                    Er.IDataProvider provider = typesAssembly.CreateInstance(customType.FullName) as Er.IDataProvider;

                    Er.DataReferenceProvider registryType = Er.DataReferenceProvider.CommonSearches
                        .FindProviderInRegistry(provider.Name, this.registryConnection);

                    if (registryType != null)
                    {
                        dataProvidersList.Add(registryType);
                    }
                }
            }

            return dataProvidersList;
        }

        /// <summary>
        /// Determines whether [is data provider] [the specified provider assembly].
        /// </summary>
        /// <param name="providerAssembly">The provider assembly.</param>
        /// <returns>
        /// 	<c>true</c> if [is data provider] [the specified provider assembly]; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsDataProvider(Assembly providerAssembly)
        {
            bool isProvider = false;

            foreach (Type type in providerAssembly.GetTypes())
            {
                if (typeof(Er.IDataProvider).IsAssignableFrom(type))
                {
                    isProvider = true;
                    break;
                }
            }

            return isProvider;
        }

        /// <summary>
        /// Creates the data provider.
        /// </summary>
        /// <param name="customDataProvider">The custom data provider.</param>
        /// 
        /// <returns></returns>
        private Er.DataReferenceProvider CreateDataProvider(Er.IDataProvider customDataProvider)
        {
            Er.DataReferenceProvider dataProvider = null;
            try
            {
                dataProvider = Er.DataReferenceProvider.Create(customDataProvider.Name, this.registryConnection);

                dataProvider.ClassName = customDataProvider.GetType().FullName;

                dataProvider.Save();
            }
            catch (Exception ex)
            {
                TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber1400000007, ex.Message);
            }

            return dataProvider;
        }

        /// <summary>
        /// Updates the data provider.
        /// </summary>
        /// <param name="registryProviderInstance">The registry provider instance.</param>
        /// <param name="dataProvider">The data provider.</param>
        private static void UpdateDataProvider(Er.DataReferenceProvider registryProviderInstance, Er.IDataProvider dataProvider)
        {
            try
            {
                registryProviderInstance.ClassName = dataProvider.GetType().FullName;

                registryProviderInstance.Save();
            }
            catch (Exception ex)
            {
                TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber1400000001, ex.Message);
            }
        }
    }
}
